FCDO UK Sanctions List Search

80%

Loading sanctions list...

Error

Uploading Report

Please wait while we save your sanctions search report...

\n", "cached": false, "entries": 11561215 }`; // Mock client data for testing const MOCK_CLIENT_DATA = { clientData: { n: { f: 'Jacob', m: 'Robert', l: 'Moran' }, b: '11-01-2001' }, userEmail: 'jacob.archer-moran@thurstanhoskin.co.uk', _id: 'test-entry-id-12345' }; // DOM elements const searchNameInput = document.getElementById('searchName'); const searchDOBInput = document.getElementById('searchDOB'); const fuzzySlider = document.getElementById('fuzzySlider'); const sliderValue = document.getElementById('sliderValue'); const includeAliasesCheckbox = document.getElementById('includeAliases'); const searchBtn = document.getElementById('searchBtn'); const resultsContainer = document.getElementById('resultsContainer'); const fixedFooter = document.getElementById('fixedFooter'); const savePdfBtn = document.getElementById('savePdfBtn'); const errorPopup = document.getElementById('errorPopup'); const errorMessage = document.getElementById('errorMessage'); const closeError = document.getElementById('closeError'); const uploadingOverlay = document.getElementById('uploadingOverlay'); // Initialize document.addEventListener('DOMContentLoaded', () => { setupEventListeners(); // Load mock client data if testing if (USE_MOCK_DATA) { console.log('🧪 Loading mock client data for testing'); clientData = MOCK_CLIENT_DATA.clientData; userEmail = MOCK_CLIENT_DATA.userEmail; itemId = MOCK_CLIENT_DATA._id; // Pre-populate form fields if (clientData.n) { const fullName = [ clientData.n.f, clientData.n.m, clientData.n.l ].filter(p => p).join(' '); searchNameInput.value = fullName; } if (clientData.b) { const parts = clientData.b.split('-'); if (parts.length === 3) { searchDOBInput.value = `${parts[2]}-${parts[1]}-${parts[0]}`; } } } loadSanctionsList(); }); // Set up event listeners function setupEventListeners() { // Slider value display fuzzySlider.addEventListener('input', (e) => { sliderValue.textContent = `${e.target.value}%`; }); // Search button searchBtn.addEventListener('click', () => { performSearch(); }); // Save PDF button savePdfBtn.addEventListener('click', () => { generateAndUploadPDF(); }); // Error popup close closeError.addEventListener('click', () => { hideError(); }); // Enter key in name field triggers search searchNameInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { performSearch(); } }); } // Load sanctions list from FCDO async function loadSanctionsList() { // Check if using mock data for testing if (USE_MOCK_DATA) { console.log('🧪 Using mock data for testing'); try { sanctionsList = parseSanctionsHTML(MOCK_SANCTIONS_HTML); console.log(`✅ Parsed ${sanctionsList.length} sanctions entries from mock data`); // Update UI to show ready state resultsContainer.innerHTML = `

✓ Sanctions list loaded (MOCK DATA)

${sanctionsList.length} entries ready to search

`; } catch (error) { console.error('❌ Error parsing mock data:', error); showError(`Error parsing mock data: ${error.message}`); } return; } try { console.log('🔍 Attempting client-side fetch of FCDO sanctions list...'); const response = await fetch('https://docs.fcdo.gov.uk/docs/UK-Sanctions-List.html'); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const html = await response.text(); console.log('✅ Client-side fetch successful, parsing...'); sanctionsList = parseSanctionsHTML(html); console.log(`✅ Parsed ${sanctionsList.length} sanctions entries`); // Update UI to show ready state resultsContainer.innerHTML = `

✓ Sanctions list loaded

${sanctionsList.length} entries ready to search

`; } catch (error) { console.error('❌ Client-side fetch failed:', error); // Try backend proxy fallback console.log('⚠️ CORS blocked - requesting HTML from parent via backend proxy'); resultsContainer.innerHTML = `

Loading via backend proxy...

`; // Request HTML from parent (parent will call backend) window.parent.postMessage({ type: 'request-sanctions-html' }, '*'); } } // Handle sanctions HTML received from parent function handleSanctionsHTML(html) { try { console.log('📥 Received sanctions HTML from parent, parsing...'); sanctionsList = parseSanctionsHTML(html); console.log(`✅ Parsed ${sanctionsList.length} sanctions entries`); // Update UI to show ready state resultsContainer.innerHTML = `

✓ Sanctions list loaded

${sanctionsList.length} entries ready to search

`; } catch (error) { console.error('❌ Error parsing sanctions HTML:', error); resultsContainer.innerHTML = `

❌ Error loading sanctions list

${error.message}

`; showError(`Error loading sanctions list: ${error.message}`); } } // Parse FCDO HTML into searchable array function parseSanctionsHTML(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // FCDO uses div-based structure, not tables // Split by
tags which separate individual entries const bodyContent = doc.body.innerHTML; const entrySections = bodyContent.split('
'); console.log(`📋 Found ${entrySections.length} entry sections`); const entries = []; entrySections.forEach((section, index) => { // Parse this section for sanctions data const tempDiv = document.createElement('div'); tempDiv.innerHTML = section; // Extract data using label matching const entry = { uniqueId: extractField(tempDiv, 'Unique ID'), name: '', aliases: [], dob: extractField(tempDiv, 'DOB'), address: extractField(tempDiv, 'Address'), nationality: extractField(tempDiv, 'Nationality') || extractField(tempDiv, 'Country'), reference: extractField(tempDiv, 'Unique ID'), regime: extractField(tempDiv, 'Regime Name'), sanctions: extractField(tempDiv, 'Sanctions Imposed'), groupId: extractField(tempDiv, 'OFSI Group ID') }; // Extract all names (primary and variations) const nameElements = tempDiv.querySelectorAll('b'); nameElements.forEach(label => { if (label.textContent.trim() === 'Name:') { let nextNode = label.nextSibling; let nameText = ''; // Collect all span siblings until next tag while (nextNode && nextNode.nodeName !== 'B') { if (nextNode.nodeName === 'SPAN') { nameText += nextNode.textContent.trim() + ' '; } nextNode = nextNode.nextSibling; } nameText = nameText.trim(); if (nameText) { if (!entry.name) { entry.name = nameText; // First name is primary } else { entry.aliases.push(nameText); // Additional names are aliases } } } }); // Convert aliases array to string entry.aliases = entry.aliases.join(', '); // Only add if has a name if (entry.name) { entries.push(entry); } }); console.log(`✅ Parsed ${entries.length} valid entries`); return entries; } // Helper function to extract field from div-based structure function extractField(container, labelText) { const labels = container.querySelectorAll('b'); for (let label of labels) { if (label.textContent.includes(labelText)) { // Find the next span sibling let nextNode = label.nextSibling; while (nextNode) { if (nextNode.nodeName === 'SPAN') { return nextNode.textContent.trim(); } nextNode = nextNode.nextSibling; if (nextNode && nextNode.nodeName === 'B') break; // Stop at next label } } } return ''; } // Perform search with fuzzy matching function performSearch() { const searchName = searchNameInput.value.trim(); if (!searchName) { showError('Please enter a name to search'); return; } if (sanctionsList.length === 0) { showError('Sanctions list not loaded yet'); return; } const threshold = parseInt(fuzzySlider.value) / 100; const includeAliases = includeAliasesCheckbox.checked; console.log(`🔍 Searching for: "${searchName}" with threshold ${threshold}`); // Store search parameters lastSearchParams = { name: searchName, dob: searchDOBInput.value, threshold: threshold, includeAliases: includeAliases, timestamp: new Date().toLocaleString('en-GB') }; // Perform fuzzy search searchResults = []; sanctionsList.forEach(entry => { let maxScore = 0; // 1. Search in full name const nameScore = calculateSimilarity(searchName.toLowerCase(), entry.name.toLowerCase()); maxScore = Math.max(maxScore, nameScore); // 2. Search first and last names separately const searchParts = searchName.toLowerCase().split(' ').filter(p => p.length > 0); const entryParts = entry.name.toLowerCase().split(' ').filter(p => p.length > 0); if (searchParts.length > 0 && entryParts.length > 0) { // Try matching first name const firstNameScore = calculateSimilarity(searchParts[0], entryParts[0]); maxScore = Math.max(maxScore, firstNameScore); // Try matching last name (if both exist) if (searchParts.length > 1 && entryParts.length > 1) { const lastNameScore = calculateSimilarity( searchParts[searchParts.length - 1], entryParts[entryParts.length - 1] ); maxScore = Math.max(maxScore, lastNameScore); // Combined first + last name score const combinedScore = (firstNameScore + lastNameScore) / 2; maxScore = Math.max(maxScore, combinedScore); } } // 3. Check for substring matches (partial contains) if (entry.name.toLowerCase().includes(searchName.toLowerCase()) || searchName.toLowerCase().includes(entry.name.toLowerCase())) { maxScore = Math.max(maxScore, 0.85); // High score for substring match } // 4. Search in aliases if enabled if (includeAliases && entry.aliases) { const aliases = entry.aliases.split(/[,;]/).map(a => a.trim()).filter(a => a); aliases.forEach(alias => { const aliasScore = calculateSimilarity(searchName.toLowerCase(), alias.toLowerCase()); maxScore = Math.max(maxScore, aliasScore); // Substring match in aliases if (alias.toLowerCase().includes(searchName.toLowerCase()) || searchName.toLowerCase().includes(alias.toLowerCase())) { maxScore = Math.max(maxScore, 0.85); } }); } if (maxScore >= threshold) { searchResults.push({ ...entry, matchScore: Math.round(maxScore * 100) }); } }); // Sort by match score (highest first) searchResults.sort((a, b) => b.matchScore - a.matchScore); console.log(`✅ Found ${searchResults.length} matches`); // Display results displayResults(); } // Calculate similarity using Levenshtein distance function calculateSimilarity(str1, str2) { const distance = levenshteinDistance(str1, str2); const maxLength = Math.max(str1.length, str2.length); return 1 - (distance / maxLength); } // Levenshtein distance algorithm function levenshteinDistance(str1, str2) { const matrix = []; for (let i = 0; i <= str2.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= str1.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= str2.length; i++) { for (let j = 1; j <= str1.length; j++) { if (str2.charAt(i - 1) === str1.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 ); } } } return matrix[str2.length][str1.length]; } // Display search results function displayResults() { if (searchResults.length === 0) { // No matches found resultsContainer.innerHTML = `
✓ No sanctions matches found

No entries in the FCDO UK Sanctions List match the search criteria.

`; } else { // Matches found let tableHTML = `
⚠️ ${searchResults.length} potential match${searchResults.length > 1 ? 'es' : ''} found
`; searchResults.forEach(result => { // Highlight matched name const highlightedName = highlightMatch(result.name, lastSearchParams.name); tableHTML += ` `; }); tableHTML += `
Score Name Aliases DOB Nationality Regime
${result.matchScore}% ${highlightedName} ${result.aliases || '—'} ${result.dob || '—'} ${result.nationality || '—'} ${result.regime || '—'}
`; resultsContainer.innerHTML = tableHTML; } // Show save PDF button fixedFooter.classList.remove('hidden'); } // Highlight matching text function highlightMatch(text, searchTerm) { if (!text || !searchTerm) return text; const regex = new RegExp(`(${searchTerm})`, 'gi'); return text.replace(regex, '$1'); } // Generate PDF and upload to S3 async function generateAndUploadPDF() { if (!lastSearchParams) { showError('No search performed yet'); return; } try { // Show uploading overlay uploadingOverlay.classList.add('show'); // Generate PDF const { jsPDF } = window.jspdf; const doc = new jsPDF('p', 'pt', 'a4'); // A4 dimensions: 595pt wide, 842pt tall const pageWidth = 595; const margin = 40; let yPosition = margin; // Header - Add company branding doc.setFont('helvetica', 'bold'); doc.setFontSize(24); doc.setTextColor(0, 60, 113); doc.text('Thurstan Hoskin', margin, yPosition); doc.setFontSize(18); doc.text("Val'ID'ate", pageWidth - margin - 100, yPosition); yPosition += 10; // Header border doc.setDrawColor(0, 60, 113); doc.setLineWidth(3); doc.line(margin, yPosition, pageWidth - margin, yPosition); yPosition += 25; // Document title doc.setFontSize(18); doc.setFont('helvetica', 'bold'); const title = 'FCDO UK Sanctions List Search'; const titleWidth = doc.getTextWidth(title); doc.text(title, (pageWidth - titleWidth) / 2, yPosition); yPosition += 25; // Search parameters doc.setFontSize(14); doc.setFont('helvetica', 'bold'); doc.text('Search Parameters', margin, yPosition); yPosition += 15; doc.setFontSize(11); doc.setFont('helvetica', 'normal'); doc.setTextColor(51, 51, 51); doc.text(`Name searched: ${lastSearchParams.name}`, margin, yPosition); yPosition += 15; if (lastSearchParams.dob) { doc.text(`Date of Birth: ${lastSearchParams.dob}`, margin, yPosition); yPosition += 15; } doc.text(`Fuzzy match threshold: ${Math.round(lastSearchParams.threshold * 100)}%`, margin, yPosition); yPosition += 15; doc.text(`Include aliases: ${lastSearchParams.includeAliases ? 'Yes' : 'No'}`, margin, yPosition); yPosition += 15; doc.text(`Search performed: ${lastSearchParams.timestamp}`, margin, yPosition); yPosition += 25; // Results section doc.setFontSize(14); doc.setFont('helvetica', 'bold'); doc.setTextColor(0, 60, 113); if (searchResults.length === 0) { doc.text('Results', margin, yPosition); yPosition += 15; doc.setFillColor(212, 237, 218); doc.setDrawColor(195, 230, 203); doc.roundedRect(margin, yPosition, pageWidth - (margin * 2), 50, 4, 4, 'FD'); doc.setFontSize(12); doc.setTextColor(21, 87, 36); doc.setFont('helvetica', 'bold'); doc.text('✓ No sanctions matches found', margin + 15, yPosition + 30); } else { doc.text(`Results (${searchResults.length} match${searchResults.length > 1 ? 'es' : ''})`, margin, yPosition); yPosition += 10; // Create table data const tableData = searchResults.map(result => [ `${result.matchScore}%`, result.name, result.aliases || '—', result.dob || '—', result.nationality || '—', result.regime || '—' ]); // Use autoTable plugin doc.autoTable({ startY: yPosition, head: [['Score', 'Name', 'Aliases', 'DOB', 'Nationality', 'Regime']], body: tableData, theme: 'striped', headStyles: { fillColor: [0, 60, 113], textColor: 255, fontSize: 10, fontStyle: 'bold' }, bodyStyles: { fontSize: 9, textColor: [51, 51, 51] }, alternateRowStyles: { fillColor: [248, 249, 250] }, margin: { left: margin, right: margin } }); yPosition = doc.lastAutoTable.finalY + 20; } // Footer doc.setFontSize(9); doc.setTextColor(102, 102, 102); doc.setFont('helvetica', 'italic'); const footerY = 820; doc.text('Data source: FCDO UK Sanctions List', margin, footerY); doc.text(`Generated by: ${userEmail}`, margin, footerY + 12); // Convert to File object const pdfBlob = doc.output('blob'); const filename = generateFilename(); const pdfFile = new File([pdfBlob], filename, { type: 'application/pdf' }); console.log('✅ PDF generated:', filename); // Upload using document-viewer.html pattern await uploadPDF(pdfFile); } catch (error) { console.error('❌ Error generating PDF:', error); uploadingOverlay.classList.remove('show'); showError(`Error generating PDF: ${error.message}`); } } // Generate filename function generateFilename() { const fe = String(clientData?.fe || 'Unknown').replace(/\//g, '-'); const clientNumber = String(clientData?.clientNumber || 'Unknown').replace(/\//g, '-'); const matterNumber = String(clientData?.matterNumber || 'Unknown').replace(/\//g, '-'); const clientName = String(clientData?.name || 'Unknown').replace(/\//g, '-'); return `${fe}-${clientNumber}-${matterNumber} - ${clientName} - FCDO Sanctions Search`; } // Upload PDF using document-viewer.html pattern async function uploadPDF(pdfFile) { try { // Create file metadata (SAME format as document-viewer.html) const fileMetadata = { type: 'PEP & Sanctions Check', document: 'FCDO Sanctions Search', uploader: userEmail, date: new Date().toLocaleString('en-GB', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }), data: { type: 'application/pdf', size: pdfFile.size, name: generateFilename(), lastModified: Date.now() }, file: pdfFile }; // Mock mode: Just download PDF instead of uploading if (USE_MOCK_DATA) { console.log('🧪 Mock mode: Downloading PDF instead of uploading'); uploadingOverlay.classList.remove('show'); // Download the PDF const pdfUrl = URL.createObjectURL(pdfFile); const link = document.createElement('a'); link.href = pdfUrl; link.download = fileMetadata.data.name + '.pdf'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(pdfUrl); showSuccess('PDF downloaded (mock mode)'); return; } // Store file locally (can't be sent via postMessage) window.pendingUploadFile = pdfFile; console.log('📤 Sending file-data to parent:', fileMetadata); // Send file-data to parent (SAME as document-viewer.html) window.parent.postMessage({ type: 'file-data', files: [fileMetadata], _id: itemId }, '*'); // Listen for put-links response (handled by message listener below) } catch (error) { console.error('❌ Error uploading PDF:', error); uploadingOverlay.classList.remove('show'); showError(`Error uploading PDF: ${error.message}`); } } // Handle put-links response from parent async function handlePutLinks(data) { try { console.log('📥 Received put-links:', data); const links = data.links; const s3Keys = data.s3Keys; if (!links || links.length === 0) { throw new Error('No PUT links received'); } // Get the file we stored locally const pdfFile = window.pendingUploadFile; if (!pdfFile) { throw new Error('PDF file not found in local storage'); } // Upload to S3 console.log('⬆️ Uploading to S3...'); const response = await fetch(links[0], { method: 'PUT', body: pdfFile, headers: { 'Content-Type': 'application/pdf' } }); if (!response.ok) { throw new Error(`S3 upload failed with status ${response.status}`); } console.log('✅ S3 upload complete'); // Send upload-success to parent (SAME as document-viewer.html) window.parent.postMessage({ type: 'upload-success', files: data.images || s3Keys, // Use images data from parent _id: itemId, uploader: userEmail }, '*'); // Clean up window.pendingUploadFile = null; uploadingOverlay.classList.remove('show'); // Show success message briefly, then close showSuccess('Report saved successfully!'); } catch (error) { console.error('❌ Error in handlePutLinks:', error); uploadingOverlay.classList.remove('show'); showError(`Upload failed: ${error.message}`); } } // Handle put-error from parent function handlePutError(data) { console.error('❌ PUT error from parent:', data); uploadingOverlay.classList.remove('show'); showError(data.message || 'Failed to generate upload link'); } // Show error popup function showError(msg) { errorMessage.textContent = msg; errorPopup.classList.add('show'); } // Hide error popup function hideError() { errorPopup.classList.remove('show'); } // Show success message function showSuccess(msg) { // Create temporary success popup const successPopup = document.createElement('div'); successPopup.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--success); color: white; padding: 20px; border-radius: 12px; font-weight: 600; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 10001; `; successPopup.textContent = msg; document.body.appendChild(successPopup); // Remove after 2 seconds setTimeout(() => { successPopup.remove(); }, 2000); } // Message listener window.addEventListener('message', (event) => { if (!event.data || !event.data.type) return; switch (event.data.type) { case 'client-data': console.log('📥 Received client-data:', event.data); clientData = event.data.clientData || {}; userEmail = event.data.userEmail || ''; itemId = event.data._id || null; // Pre-populate name field if (clientData.n) { const fullName = [ clientData.n.f, clientData.n.m, clientData.n.l ].filter(p => p).join(' '); searchNameInput.value = fullName; } // Pre-populate DOB if available if (clientData.b) { // Convert dd-mm-yyyy to yyyy-mm-dd for date input const parts = clientData.b.split('-'); if (parts.length === 3) { searchDOBInput.value = `${parts[2]}-${parts[1]}-${parts[0]}`; } } console.log('✅ Client data loaded, ready to search'); break; case 'sanctions-html': // Received HTML from backend proxy console.log('📥 Received sanctions HTML from parent'); handleSanctionsHTML(event.data.html); break; case 'put-links': handlePutLinks(event.data); break; case 'put-error': handlePutError(event.data); break; } }); console.log('✅ FCDO Sanctions Search iframe loaded');